package checker

import (
	"mi/compiler/ast"
	"mi/compiler/binder"
	"mi/compiler/token"
)

func (c *checker) getAssignedTypeOfBinaryExpression(node *ast.BinaryExpression) Type {
	return c.getTypeOfExpression(node.Right)
}

func (c *checker) getAssignedType(node ast.Node) Type {
	var parent = node.Parent()
	switch pn := parent.(type) {
	case *ast.BinaryExpression:
		return c.getAssignedTypeOfBinaryExpression(pn)
	}
	return c.unknownType
}

func (c *checker) getFlowTypeOfInitializer(node ast.Expression) Type {
	// Return the cached type if one is available. If the type of the variable was inferred
	// from its initializer, we'll already have cached the type. Otherwise we compute it now
	// without caching such that transient types are reflected.
	var links = c.getNodeLinks(node)
	if !isNull(links.resolvedFlowType) {
		return links.resolvedFlowType
	}

	if !isNull(links.resolvedType) {
		return links.resolvedType
	}

	var declType = c.getTypeOfExpression(node)
	var flowType = c.getNodeLinks(node).resolvedFlowType
	if !isNull(flowType) {
		return flowType
	}
	return declType
}

func (c *checker) getInitialFlowTypeOfVariableDeclaration(node *ast.VariableDeclaration) Type {
	if !isNull(node.Initializer) {
		return c.getFlowTypeOfInitializer(node.Initializer)
	}
	return c.unknownType
}

func (c *checker) getInitialFlowType(node *ast.VariableDeclaration) Type {
	return c.getInitialFlowTypeOfVariableDeclaration(node)
}

func (c *checker) getInitialOrAssignedType(node ast.Node) Type {
	switch n := node.(type) {
	case *ast.VariableDeclaration:
		return c.getInitialFlowType(n)
	default:
		return c.getAssignedType(node)
	}
}

func (c *checker) getFlowTypeOfReference(reference ast.Node, declaredType Type, assumeInitialized bool, flowContainer ast.Node) Type {
	var flow = binder.GetFlowOfNode(reference)
	if flow == nil && declaredType.Flags()&TFNarrowable == 0 {
		return declaredType
	}

	var initialType Type
	if assumeInitialized {
		initialType = declaredType
	} else {
		initialType = c.undefinedType
	}

	var visitedFlowStart = c.visitedFlowCount
	var ctx = narrowContext{
		checker:           c,
		key:               "",
		reference:         reference,
		declaredType:      declaredType,
		assumeInitialized: assumeInitialized,
		flowContainer:     flowContainer,
		initialType:       initialType,
		visitedFlowStart:  visitedFlowStart,
	}
	var evolvedType = c.getTypeFromFlowType(ctx.getTypeAtFlowNode(flow))
	c.visitedFlowCount = visitedFlowStart
	return evolvedType
}


func (c *checker) getFlowNodeId(flow binder.FlowNode) int {
	if flow.ID() == 0 {
		flow.SetID(nextFlowId)
		nextFlowId++
	}
	return flow.ID()
}

type narrowContext struct {
	checker *checker
	key string
	reference ast.Node
	declaredType Type
	assumeInitialized bool
	flowContainer ast.Node
	initialType Type
	visitedFlowStart int
}

func (n *narrowContext) getTypeAtFlowNode(flow binder.FlowNode) Type {
	for true {
		if flow.Flags()&binder.FFShared != 0 {
			// We cache results of flow type resolution for shared nodes that were previously visited in
			// the same getFlowTypeOfReference invocation. A ast is considered shared when it is the
			// antecedent of more than one ast.
			for i := n.visitedFlowStart; i < n.checker.visitedFlowCount; i++ {
				if n.checker.visitedFlowNodes[i] == flow {
					return n.checker.visitedFlowTypes[i]
				}
			}
		}
		var tpe Type
		switch f := flow.(type) {
		case *binder.AfterFinallyFlow:
			// block flow edge: finally -> pre-try (for larger explanation check comment in binder.ts - bindTryStatement
			f.Lock()
			tpe = n.getTypeAtFlowNode(f.Antecedent)
			f.UnLock()
		case *binder.PreFinallyFlow:
			// locked pre-finally flows are filtered out in getTypeAtFlowBranchLabel
			// so here just redirect to antecedent
			flow = f.Antecedent
			continue
		case *binder.FlowAssignment:
			tpe = n.getTypeAtFlowAssignment(f)
			if tpe == nil {
				flow = f.Antecedent
				continue
			}
		case *binder.FlowCondition:
			tpe = n.getTypeAtFlowCondition(f)
		case *binder.FlowLabel:
			if len(f.Antecedents) == 1 {
				flow = f.Antecedents[0]
				continue
			}
			if f.Kind == binder.BranchLabel {
				tpe = n.getTypeAtFlowBranchLabel(f)
			} else {
				tpe = n.getTypeAtFlowLoopLabel(f)
			}
		case *binder.FlowStart:
			// Check if we should continue with the control flow of the containing function.
			var container = f.Container
			if container != nil && container != n.flowContainer && !ast.IsSelectorExpression(n.reference) {
				flow = binder.GetFlowOfNode(container)
				continue
			}
			// At the top of the flow we have the initial type.
			tpe = n.initialType
		default:
			// Unreachable code errors are reported in the binding phase. Here we
			// simply return the non-auto declared type to reduce follow-on errors.
			tpe = n.declaredType
		}

		if flow.Flags()&binder.FFShared != 0 {
			// Record visited ast and the associated type in the cache.
			n.checker.visitedFlowNodes[n.checker.visitedFlowCount] = flow
			n.checker.visitedFlowTypes[n.checker.visitedFlowCount] = tpe
			n.checker.visitedFlowCount++
		}
		return tpe
	}
	
	
	return nil
}

func (n *narrowContext) containsMatchingReference(source ast.Node, target ast.Node) bool {
	for ast.IsSelectorExpression(source) {
		source = source.(*ast.SelectorExpression).Expression
		if n.checker.isMatchingReference(source, target) {
			return true
		}
	}
	return false
}

func (n *narrowContext) getTypeAtFlowAssignment(flow *binder.FlowAssignment) Type {
	var node = flow.Node
	// Assignments only narrow the computed type if the declared type is a union type. Thus, we
	// only need to evaluate the assigned type if the declared type is a union type.
	if n.checker.isMatchingReference(n.reference, node) {
		if ast.GetAssignmentTargetKind(node) == ast.Compound {
			var flowType = n.getTypeAtFlowNode(flow.Antecedent)
			if flowType.Flags()&TFNullable != 0 {
				var tpe = n.checker.getInitialOrAssignedType(node)
				if tpe != n.checker.unknownType&& tpe.Flags()&TFNullable == 0 {
					return n.checker.getNotNullType(flowType)
				}
			}
		}
		if ast.IsVariableDeclaration(node) {
			return n.checker.getInitialOrAssignedType(node)
		}
		return n.declaredType
	}
	// We didn't have a direct match. However, if the reference is a dotted name, this
	// may be an assignment to a left hand part of the reference. For example, for a
	// reference 'x.y.z', we may be at an assignment to 'x.y' or 'x'. In that case,
	// return the declared type.
	if n.containsMatchingReference(n.reference, node) {
		return n.declaredType
	}
	// Assignment doesn't affect reference
	return nil
}

func (n *narrowContext) getTypeAtFlowCondition(flow *binder.FlowCondition) Type {
	var flowType = n.getTypeAtFlowNode(flow.Antecedent)
	var assumeTrue = flow.Value
	return n.narrowType(flowType, flow.Expr, assumeTrue)
}

func (n *narrowContext) getTypesFlags(types []Type) TypeFlags {
	var flags TypeFlags
	for _, t := range types {
		flags |= t.Flags()
	}
	return flags
}

func (n *narrowContext) allTypeIsIdentical(types []Type) bool {
	if len(types) == 0 {
		return true
	}

	var first = types[0]
	for _, t := range types {
		if first != t {
			 return false
		}
	}
	return true
}

func (n *narrowContext) hasUndefined(types_ []Type) bool {
	for _, t := range types_ {
		if t == n.checker.undefinedType {
			return true
		}
	}
	return false
}

func (n *narrowContext) getTypeAtFlowBranchLabel(flow *binder.FlowLabel) Type {
	var antecedentTypes []Type
	for _, antecedent := range flow.Antecedents {
		if binder.IsPreFinallyFlow(antecedent) && antecedent.(*binder.PreFinallyFlow).Lock.Locked() {
			// if flow correspond to branch from pre-try to finally and this branch is locked - this means that
			// we initially have started following the flow outside the finally block.
			// in this case we should ignore this branch.
			continue
		}
		var flowType = n.getTypeAtFlowNode(antecedent)
		// If the type at a particular antecedent path is the declared type and the
		// reference is known to always be assigned (i.e. when declared and initial types
		// are the same), there is no reason to process more antecedents since the only
		// possible outcome is subtypes that will be removed in the final union type anyway.
		if flowType == n.declaredType && n.declaredType == n.initialType {
			return n.declaredType
		}
		if !containsType(antecedentTypes, flowType) {
			antecedentTypes = append(antecedentTypes, flowType)
		}
	}

	if len(antecedentTypes) != 0 {
		if n.allTypeIsIdentical(antecedentTypes) {
			return antecedentTypes[0]
		} else if n.hasUndefined(antecedentTypes) {
			return n.checker.undefinedType
		} else if n.getTypesFlags(antecedentTypes)&TFNullable == 0 {
			return n.checker.getNotNullType(n.declaredType)
		}
	}

	return n.declaredType
}

func (n *narrowContext) getTypeAtFlowLoopLabel(flow *binder.FlowLabel) Type {
	// If we have previously computed the control flow type for the reference at
	// this flow loop junction, return the cached type.
	var id = n.checker.getFlowNodeId(flow)
	if n.checker.flowLoopCaches[id] == nil {
		n.checker.flowLoopCaches[id] = make(map[string]Type)
	}
	var cache = n.checker.flowLoopCaches[id]
	if len(n.key) == 0 {
		n.key = n.checker.getFlowCacheKey(n.reference)
	}
	var cached = cache[n.key]
	if cached != nil {
		return cached
	}
	// If this flow loop junction and reference are already being processed, return
	// the union of the types computed for each branch so far, marked as incomplete.
	// It is possible to see an empty array in cases where loops are nested and the
	// back edge of the outer loop reaches an inner loop that is already being analyzed.
	// In such cases we restart the analysis of the inner loop, which will then see
	// a non-empty in-process array for the outer loop and eventually terminate because
	// the first antecedent of a loop junction is always the non-looping control flow
	// path that leads to the top.
	for i := n.checker.flowLoopStart; i < n.checker.flowLoopCount; i++ {
		if n.checker.flowLoopNodes[i] == flow && n.checker.flowLoopKeys[i] == n.key && len(n.checker.flowLoopTypes[i]) > 0 {
			if n.allTypeIsIdentical(n.checker.flowLoopTypes[i]) {
				return n.checker.createFlowType(n.checker.flowLoopTypes[i][0], true)
			}
			if n.getTypesFlags(n.checker.flowLoopTypes[i])&TFNullable == 0 {
				return n.checker.createFlowType(n.checker.getNotNullType(n.declaredType), true)
			}
			return n.checker.createFlowType(n.declaredType, true)
		}
	}

	// Add the flow loop junction and reference to the in-process stack and analyze
	// each antecedent code path.
	var firstAntecedentType Type
	n.checker.flowLoopNodes[n.checker.flowLoopCount] = flow
	n.checker.flowLoopKeys[n.checker.flowLoopCount] = n.key
	for _, antecedent := range flow.Antecedents {
		n.checker.flowLoopCount++
		var flowType = n.getTypeAtFlowNode(antecedent)
		n.checker.flowLoopCount--
		if firstAntecedentType == nil {
			firstAntecedentType = flowType
		}
		var tpe = n.checker.getTypeFromFlowType(flowType)
		// If we see a value appear in the cache it is a sign that control flow  analysis
		// was restarted and completed by checkExpressionCached. We can simply pick up
		// the resulting type and bail out.
		var cached = cache[n.key]
		if cached != nil {
			return cached
		}
		if !containsType(n.checker.flowLoopTypes[n.checker.flowLoopCount], tpe) {
			n.checker.flowLoopTypes[n.checker.flowLoopCount] = append(n.checker.flowLoopTypes[n.checker.flowLoopCount], tpe)
		}

		// If the type at a particular antecedent path is the declared type there is no
		// reason to process more antecedents since the only possible outcome is subtypes
		// that will be removed in the final union type anyway.
		if tpe == n.declaredType {
			break
		}
	}
	// The result is incomplete if the first antecedent (the non-looping control flow path)
	// is incomplete.
	var result Type
	if n.allTypeIsIdentical(n.checker.flowLoopTypes[n.checker.flowLoopCount]) {
		result =  n.checker.flowLoopTypes[n.checker.flowLoopCount][0]
	} else if n.hasUndefined(n.checker.flowLoopTypes[n.checker.flowLoopCount]) {
		result = n.checker.undefinedType
	} else if n.getTypesFlags(n.checker.flowLoopTypes[n.checker.flowLoopCount])&TFNullable == 0 {
		result = n.checker.getNotNullType(n.declaredType)
	} else {
		result = n.declaredType
	}
	if n.checker.isIncomplete(firstAntecedentType) {
		return n.checker.createFlowType(result, true)
	}
	cache[n.key] = result
	return result
}

func (n *narrowContext) getReferenceCandidate(node ast.Expression) ast.Expression {
	switch _n := node.(type) {
	case *ast.ParenthesizedExpression:
		return n.getReferenceCandidate(_n.Expression)
	}
	return node
}

func (n *narrowContext) narrowTypeByTruthiness(tpe Type, expr ast.Expression, assumeTrue bool) Type {
	if tpe.Flags()&TFBoolean != 0 && n.checker.isMatchingReference(n.reference, expr) {
		if assumeTrue {
			return n.checker.trueType
		} else {
			return n.checker.falseType
		}
	}
	return tpe
}

func (n *narrowContext) narrowTypeByBinaryExpression(tpe Type, expr *ast.BinaryExpression, assumeTrue bool) Type {
	switch expr.Operator.Token {
		case token.EqualsEquals, 		// ==
		 token.ExclamationEquals:      	// !=
			var operator = expr.Operator.Token
			var left = n.getReferenceCandidate(expr.Left)
			var right = n.getReferenceCandidate(expr.Right)
			if n.checker.isMatchingReference(n.reference, left) {
				return n.narrowTypeByEquality(tpe, operator, right, assumeTrue)
			}
			if n.checker.isMatchingReference(n.reference, right) {
				return n.narrowTypeByEquality(tpe, operator, left, assumeTrue)
			}
		case token.InstanceOfKeyword:
			return n.narrowTypeByInstanceof(tpe, expr, assumeTrue)
	}
	return tpe
}

func (n *narrowContext) narrowTypeByEquality(tpe Type, operator token.Token, value ast.Expression, assumeTrue bool) Type {
	if tpe.Flags()&TFAny != 0 {
		return tpe
	}
	if operator == token.ExclamationEquals {
		assumeTrue = !assumeTrue
	}
	var valueType = n.checker.getTypeOfExpression(value)
	if valueType.Flags()&TFNull != 0 {
		if assumeTrue {
			return n.checker.getNullableType(tpe)
		} else {
			return n.checker.getNotNullType(tpe)
		}
	}
	if tpe.Flags()&TFNullable != 0 && valueType.Flags()&TFNullable == 0 && assumeTrue {
		return n.checker.getNotNullType(tpe)
	}
	//if valueType.Flags()&TFLiteral != 0 && n.checker.areTypesComparable(tpe, valueType) {
	//	return valueType
	//}
	return tpe
}

func (n *narrowContext) narrowTypeByInstanceof(tpe Type, expr *ast.BinaryExpression, assumeTrue bool) Type {
	var left = n.getReferenceCandidate(expr.Left)
	if !n.checker.isMatchingReference(n.reference, left) {
		// For a reference of the form 'x.y', an 'x instanceof T' type guard resets the
		// narrowed type of 'y' to its declared type.
		if n.containsMatchingReference(n.reference, left) {
			return n.declaredType
		}
		return tpe
	}

	// Check that right operand is a function type with a prototype property
	var rightType = n.checker.getTypeOfExpression(expr.Right)
	return rightType
}

// Narrow the given type based on the given expression having the assumed boolean value. The returned type
// will be a subtype or the same type as the argument.
func (n *narrowContext) narrowType(tpe Type, expr ast.Expression, assumeTrue bool) Type {
	switch node := expr.(type) {
	case *ast.Identifier,
		*ast.ThisExpression,
		*ast.SelectorExpression:
			return n.narrowTypeByTruthiness(tpe, expr, assumeTrue)
	case *ast.ParenthesizedExpression:
		return n.narrowType(tpe, node.Expression, assumeTrue)
	case *ast.BinaryExpression:
		return n.narrowTypeByBinaryExpression(tpe, node, assumeTrue)
	case *ast.PrefixUnaryExpression:
		if node.Operator.Token == token.Exclamation {
			return n.narrowType(tpe, node.Operand, !assumeTrue)
		}
	}
	return tpe
}
